﻿using Dycox;
using Dycox.Linq.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Dycox.Workflows
{
    public class WorkflowUndoContext : WorkflowExecutionContext
    {
        internal WorkflowUndoContext(Workflow workflow, OperationArguments args, DataProvider dataProvider)
            : base(workflow, args)
        {
            if (dataProvider == null)
                throw new ArgumentNullException("dataProvider");

            base.SetDataProvider(dataProvider);
        }

        private WorkflowUndoState _undoState;

        public WorkflowUndoState UndoState
        {
            get { return _undoState; }
        }

        private WFFrame _frame;

        internal WFFrame Frame
        {
            get { return _frame; }
        }

        internal override void BeginExecute()
        {
            DataProvider provider = DataProvider;
            provider.ValidateNoChanges();

            _frame = provider.PopFrame(Arguments.ComponentID.ProcessId);

            base.BeginExecute();

            WFProcess process = this.Process;// WorkflowManager.EnsureProcess(provider.GetComponent(this.Arguments.ComponentID, true));

            if (process.WorkflowName != this.Workflow.Name)
                throw new ArgumentException(SR.GetString("invalid_workflow_name", process.WorkflowName));

            //if (frame.StageType == StageUsage.Entrance)
            //    throw new WorkflowExecutionException(SR.GetString("cannot_undo_entrance"));

            _undoState = WorkflowUndoState.Decompress(_frame.UndoData.ToArray());

            ValidateChangeConflict();

        }

        protected override IWorkflowComponent GetContextComponent()
        {
            return DataProvider.GetComponent(ComponentIdentifier.Parse(_frame.StepId), true);
        }

        public void RestoreOriginalState(object entity)
        {
            if (entity == null)
                throw new ArgumentNullException("entity");

            var info = GetChangeInfo(entity);
            RestoreUpdated(entity, info);

        }

        /// <summary>
        /// Restores orginal state accroding to the <see cref="P:UndoSstate"/> contents.
        /// </summary>
        public void Restore()
        {
            RestoreDeletes();
            RestoreUpdates();
            RestoreInserts();
        }

        private void RestoreUpdated(object entity, EntityChangeInfo info)
        {
            var original = info.Original;
            var type = entity.GetType();

            foreach (string name in info.MemberNames)
            {
                var member = type.GetPropertyOrField(name);
                var value = member.GetValue(original);

                member.SetValue(entity, value);

            }
        }

        private void RestoreUpdates()
        {
            var updates = _undoState.Updates;
            if (updates != null)
            {
                foreach (var info in updates)
                {
                    object entity = GetFreshEntity(info.Original);
                    RestoreUpdated(entity, info);
                }
            }
        }

        private void RestoreInserts()
        {
            var inserted = _undoState.Inserts;

            if (inserted != null)
            {
                foreach (var obj in inserted.Reverse())
                {
                    var key = DataProvider.GetEntityKey(obj);
                    this.DataProvider.DeleteEntity(key, obj.GetType());
                }
            }
        }

        private void RestoreDeletes()
        {
            var deletes = _undoState.Deletes;

            if (deletes != null)
            {
                throw new NotImplementedException("Restoring deleted entities is not implemented yet.");
            }
        }

        private object GetFreshEntity(object obj)
        {
            return DataProvider.GetEntity(DataProvider.GetEntityKey(obj), obj.GetType());
        }

        public object GetOriginalEntity(object entity)
        {
            return GetChangeInfo(entity).Original;
        }

        public T GetOriginalEntity<T>(T entity)
        {
            return (T)GetChangeInfo(entity).Original;
        }

        private EntityChangeInfo GetChangeInfo(object entity)
        {
            if (entity == null)
                throw new ArgumentNullException("entity");

            var updates = _undoState.Updates;
            
            if (updates != null)
            {
                var key = DataProvider.GetEntityKey(entity);

                foreach (var info in updates)
                {
                    object obj = info.Original;
                    if (obj.GetType() == entity.GetType())
                    {
                        var key0 = DataProvider.GetEntityKey(obj);
                        if (key.Equals(key0))
                            return info;
                    }
                }
            }

            throw new WorkflowException(SR.GetString("change_info_not_found"));
        }

        private void ValidateChangeConflict()
        {
            var updates = _undoState.Updates;

            if (updates != null)
            {
                DataProvider provider = this.DataProvider;

                foreach (var info in updates)
                {
                    object last = info.New, current;
                    Type type = last.GetType();

                    current = provider.GetEntity(provider.GetEntityKey(last), type);

                    foreach (string name in info.MemberNames)
                    {
                        var m = type.GetPropertyOrField(name);

                        if (m == null)
                            throw new WorkflowException(SR.GetString("member_not_found", name));
                        object v0, v1;

                        v0 = m.GetValue(last);
                        v1 = m.GetValue(current);

                        if (!object.Equals(v0, v1))
                        {
                            bool changed = true;

                            // DateTime fix
                            Type vt = (v0 ?? v1).GetType();
                            if (vt == typeof(DateTime))
                            {
                                changed = Math.Abs(((DateTime)v0 - (DateTime)v1).TotalSeconds) > 60;
                            }

                            if (changed)
                                throw new WorkflowExecutionException(SR.GetString("fallback_change_conflict", type.FullName, name));
                        }
                    }
                }
            }
        }

    }
}
